No3. Cloudflare 真實訪客 IP:Nginx real_ip 與 Apache2 RemoteIPTrustedProxyList

網站使用 Cloudflare Proxy 後,真正和 Origin 建立 TCP 連線的是 Cloudflare Edge,不是訪客的瀏覽器。因此,Nginx 或 Apache2 在沒有額外設定時,Access Log 記錄 Cloudflare IP 是正常結果。

Cloudflare 會透過 CF-Connecting-IP 將訪客 IP 傳給 Origin,但這個值仍然是 HTTP Header。Origin 不能只因為 Header 名稱看起來像 Cloudflare,就直接把它當成可信的 Client IP。

安全的處理方式包含兩項設定:

Cloudflare CIDR
    ├── 可以連到 Origin
    └── 可以透過 CF-Connecting-IP 提供 Visitor IP

其他來源
    ├── 不應直接連到 Origin
    └── 即使自行加入 CF-Connecting-IP,也不能改寫 Client IP

本文以 Ubuntu 24.04 VM 為環境,分別示範 Nginx real_ip 與 Apache2 mod_remoteip。兩種 Web Server 擇一設定即可。

前半篇固定採用下列前提:GCP Firewall 已限制只有 Cloudflare CIDR 可以連入 Origin 的 80443,Nginx 與 Apache2 不負責來源阻擋,只處理 Trusted Proxy 與 Visitor IP。來源限制若設在 Web Server,會在 Nginx、Apache2 與 systemd 設定完成並驗證後獨立說明。

Visitor
   │
   ▼
Cloudflare Edge
   │ Cloudflare source IP
   ▼
GCP Firewall(只允許 Cloudflare CIDR)
   │
   ▼
Origin VM(Nginx 或 Apache2)

Cloudflare Proxy 後的 Client IP

連線經過 Cloudflare 後,會同時出現三種 IP:

名稱 取得位置 用途
Visitor IP CF-Connecting-IP Access Log、GeoIP、Rate Limit 與 Application Log
Connection peer IP TCP connection 確認實際連入 Origin 的 Proxy
Web Server Client IP Nginx $remote_addr 或 Apache2 %a Web Server 經 trusted proxy 規則處理後採用的 Client IP

假設訪客 IP 是 198.51.100.25,Cloudflare Edge IP 是 173.245.48.10

Visitor 198.51.100.25
        │
        ▼
Cloudflare 173.245.48.10
        │
        │ CF-Connecting-IP: 198.51.100.25
        ▼
Origin VM

尚未啟用 real-IP module 時,Web Server 看到的 Client IP 是 173.245.48.10。啟用後則應得到:

Visitor IP:198.51.100.25
Peer IP:173.245.48.10

Peer IP 不應被 Visitor IP 完全取代。它仍是判斷 request 是否由 Cloudflare 傳入,以及排查 Proxy 路徑的重要資料。

CF-Connecting-IP 的信任條件

一般 HTTP client 也能自行建立同名 Header:

curl \
  --header 'CF-Connecting-IP: 198.51.100.77' \
  'http://127.0.0.1/'

因此,CF-Connecting-IP 能否改寫 Client IP,必須由 TCP peer 決定:

  1. Origin 只接受 Cloudflare 或其他明確核准的來源。
  2. Nginx set_real_ip_from 或 Apache2 RemoteIPTrustedProxyList 只包含 Cloudflare 官方 CIDR。
  3. Application 使用 Web Server 已驗證的 Client IP,不再自行信任任意 X-Forwarded-For

Cloudflare 將 CF-Connecting-IP 定義為連到 Cloudflare 的訪客 IP。相較之下,X-Forwarded-For 可能包含多個 Proxy hop,也可能保留 client 原先傳入的內容;本文的單層 Cloudflare 架構直接使用 CF-Connecting-IPCloudflare:HTTP request headers

下列設定不能視為等價:

# 錯誤:允許任何來源提供 Client IP
set_real_ip_from 0.0.0.0/0;
set_real_ip_from ::/0;
# 不完整:只有指定 Header,沒有 Trusted Proxy
RemoteIPHeader CF-Connecting-IP

Cloudflare CIDR 可能調整,官方建議 Origin allowlist 與 Proxy trust 使用最新的 Cloudflare IP ranges。Cloudflare:Cloudflare IP addresses

Pseudo IPv4

Cloudflare 的 Pseudo IPv4 若設為 Overwrite Headers,IPv6 訪客的 CF-Connecting-IP 會被改成 Class E IPv4,原始 IPv6 則位於 CF-Connecting-IPv6。需要保留原始 IPv6 時,必須先確認目前 Cloudflare Network 設定;不能看到 IPv4 格式就假設訪客真的使用 IPv4。Cloudflare:Pseudo IPv4 headers

本文其餘範例假設未使用 Overwrite Headers

Nginx real_ip 設定

確認 Real IP Module 與設定位置

在 Origin VM 執行:

sudo nginx -V 2>&1 | grep -- '--with-http_realip_module'
sudo nginx -T

/第一個指令應找到 --with-http_realip_module。Ubuntu 官方 Nginx package 通常已包含此 module;自訂編譯版本仍應實際確認。

接著確認 /etc/nginx/nginx.conf 的 http context 內包含:

include /etc/nginx/conf.d/*.conf;

本文把固定 real-IP 設定放在 /etc/nginx/conf.d/cloudflare-real-ip.conf,每日產生的 Cloudflare CIDR 則放在 /etc/nginx/snippets/

建立 Nginx Trusted Proxy Updater

安裝工具:

sudo apt update
sudo apt install --yes curl diffutils jq util-linux

建立 updater:

sudo nano /usr/local/sbin/update-cloudflare-nginx-real-ip

內容如下:

#!/usr/bin/env bash
set -Eeuo pipefail

if [[ "$EUID" -ne 0 ]]; then
  echo 'Run this updater as root.' >&2
  exit 1
fi

TRUSTED_FILE='/etc/nginx/snippets/cloudflare-real-ip-trusted.conf'
API_URL='https://api.cloudflare.com/client/v4/ips'
WORK_DIRECTORY="$(mktemp -d)"

cleanup() {
  rm -rf -- "$WORK_DIRECTORY"
}
trap cleanup EXIT

curl \
  --fail \
  --silent \
  --show-error \
  --connect-timeout 5 \
  --max-time 20 \
  --retry 3 \
  --output "$WORK_DIRECTORY/response.json" \
  "$API_URL"

jq --exit-status '
  .success == true
  and ((.result.ipv4_cidrs | type) == "array")
  and ((.result.ipv4_cidrs | length) > 0)
  and ((.result.ipv6_cidrs | type) == "array")
  and ((.result.ipv6_cidrs | length) > 0)
' "$WORK_DIRECTORY/response.json" >/dev/null

jq --raw-output '
  .result.ipv4_cidrs[],
  .result.ipv6_cidrs[]
' "$WORK_DIRECTORY/response.json" \
  | sort --unique > "$WORK_DIRECTORY/cloudflare-ips.lst"

[[ -s "$WORK_DIRECTORY/cloudflare-ips.lst" ]]

awk '{ print "set_real_ip_from " $0 ";" }' \
  "$WORK_DIRECTORY/cloudflare-ips.lst" \
  > "$WORK_DIRECTORY/cloudflare-real-ip-trusted.conf"

install -d -m 0755 /etc/nginx/snippets

if [[ -f "$TRUSTED_FILE" ]] \
  && cmp --silent "$WORK_DIRECTORY/cloudflare-real-ip-trusted.conf" "$TRUSTED_FILE"; then
  echo 'Cloudflare trusted proxy list unchanged.'
  exit 0
fi

install -m 0644 \
  "$WORK_DIRECTORY/cloudflare-real-ip-trusted.conf" \
  "$TRUSTED_FILE"

nginx -t
systemctl reload nginx

echo 'Cloudflare trusted proxy list updated for Nginx.'

設定權限並先產生第一份清單:

sudo chown root:root /usr/local/sbin/update-cloudflare-nginx-real-ip
sudo chmod 0755 /usr/local/sbin/update-cloudflare-nginx-real-ip
sudo /usr/local/sbin/update-cloudflare-nginx-real-ip

cloudflare-real-ip-trusted.conf 會是:

set_real_ip_from 173.245.48.0/20;
set_real_ip_from 103.21.244.0/22;
# 其餘 Cloudflare IPv4、IPv6 CIDR

設定 Client IP 與 Access Log

建立 /etc/nginx/conf.d/cloudflare-real-ip.conf

sudo nano /etc/nginx/conf.d/cloudflare-real-ip.conf
include /etc/nginx/snippets/cloudflare-real-ip-trusted.conf;
real_ip_header CF-Connecting-IP;

log_format cloudflare_realip
    'visitor=$remote_addr peer=$realip_remote_addr '
    'cf_ip=$http_cf_connecting_ip cf_ray=$http_cf_ray '
    '"$request" $status $body_bytes_sent';

set_real_ip_from 決定哪些 connection peer 有權提供 CF-Connecting-IP。Nginx 完成改寫後:

  • $remote_addr 是 Visitor IP。
  • $realip_remote_addr 保留原始 connection peer。
  • $http_cf_connecting_ip 是收到的 Header,適合用來交叉檢查,不應單獨作為授權依據。

real_ip_recursive 預設為 off。本文使用單值的 CF-Connecting-IP,不需要啟用 X-Forwarded-For chain 的遞迴解析。NGINX:Real IP module

在網站的 server block 指定新的 log format:

server {
    listen 443 ssl;
    server_name www.example.com;

    access_log /var/log/nginx/www.example.com.access.log cloudflare_realip;

    # 既有 TLS、root 或 proxy_pass 設定。
}

檢查並套用:

sudo nginx -t
sudo systemctl reload nginx

若 Nginx 後方還有 Application server,應將已驗證的 $remote_addr 明確傳給下游,避免 Application 再解析 client 自行建立的 X-Forwarded-For chain:

location / {
    proxy_set_header X-Real-IP $remote_addr;
    proxy_set_header X-Forwarded-For $remote_addr;
    proxy_pass http://127.0.0.1:8000;
}

Apache2 mod_remoteip 設定

啟用 mod_remoteip

在 Origin VM 執行:

sudo apache2ctl -S
sudo a2enmod remoteip
sudo apache2ctl -M | grep remoteip

正常會看到:

remoteip_module (shared)

Apache 2.4 的 mod_remoteip 會讓還原後的 Visitor IP 參與 logging、authorization 與其他 module 的處理;原始 connection peer 仍可透過 %{c}a 或 expression 內的 CONN_REMOTE_ADDR 取得。Apache HTTP Server:mod_remoteip

建立 Apache2 Trusted Proxy Updater

安裝工具:

sudo apt update
sudo apt install --yes curl diffutils jq util-linux

建立 updater:

sudo nano /usr/local/sbin/update-cloudflare-apache-real-ip

內容如下:

#!/usr/bin/env bash
set -Eeuo pipefail

if [[ "$EUID" -ne 0 ]]; then
  echo 'Run this updater as root.' >&2
  exit 1
fi

TRUSTED_FILE='/etc/apache2/cloudflare-ips.lst'
API_URL='https://api.cloudflare.com/client/v4/ips'
WORK_DIRECTORY="$(mktemp -d)"

cleanup() {
  rm -rf -- "$WORK_DIRECTORY"
}
trap cleanup EXIT

curl \
  --fail \
  --silent \
  --show-error \
  --connect-timeout 5 \
  --max-time 20 \
  --retry 3 \
  --output "$WORK_DIRECTORY/response.json" \
  "$API_URL"

jq --exit-status '
  .success == true
  and ((.result.ipv4_cidrs | type) == "array")
  and ((.result.ipv4_cidrs | length) > 0)
  and ((.result.ipv6_cidrs | type) == "array")
  and ((.result.ipv6_cidrs | length) > 0)
' "$WORK_DIRECTORY/response.json" >/dev/null

jq --raw-output '
  .result.ipv4_cidrs[],
  .result.ipv6_cidrs[]
' "$WORK_DIRECTORY/response.json" \
  | sort --unique > "$WORK_DIRECTORY/cloudflare-ips.lst"

[[ -s "$WORK_DIRECTORY/cloudflare-ips.lst" ]]

if [[ -f "$TRUSTED_FILE" ]] \
  && cmp --silent "$WORK_DIRECTORY/cloudflare-ips.lst" "$TRUSTED_FILE"; then
  echo 'Cloudflare trusted proxy list unchanged.'
  exit 0
fi

install -m 0644 \
  "$WORK_DIRECTORY/cloudflare-ips.lst" \
  "$TRUSTED_FILE"

apache2ctl configtest
systemctl reload apache2

echo 'Cloudflare trusted proxy list updated for Apache2.'

設定權限並產生第一份清單:

sudo chown root:root /usr/local/sbin/update-cloudflare-apache-real-ip
sudo chmod 0755 /usr/local/sbin/update-cloudflare-apache-real-ip
sudo /usr/local/sbin/update-cloudflare-apache-real-ip

/etc/apache2/cloudflare-ips.lst 的格式是一行一個 CIDR:

103.21.244.0/22
173.245.48.0/20
# 其餘 Cloudflare IPv4、IPv6 CIDR

RemoteIPTrustedProxyList 會在 Apache 啟動或 reload 時讀取這份檔案。

設定 RemoteIPHeader 與 Access Log

建立 /etc/apache2/conf-available/cloudflare-real-ip.conf

sudo nano /etc/apache2/conf-available/cloudflare-real-ip.conf
RemoteIPHeader CF-Connecting-IP
RemoteIPTrustedProxyList /etc/apache2/cloudflare-ips.lst

LogFormat "visitor=%a peer=%{c}a cf_ip=%{CF-Connecting-IP}i cf_ray=%{CF-Ray}i \"%r\" %>s %b" cloudflare_realip

啟用設定:

sudo a2enconf cloudflare-real-ip
sudo apache2ctl configtest
sudo systemctl reload apache2

在目標 VirtualHost 使用新的 log format:

<VirtualHost *:443>
    ServerName www.example.com

    CustomLog ${APACHE_LOG_DIR}/www.example.com.access.log cloudflare_realip

    # 既有 TLS、DocumentRoot 或 ProxyPass 設定。
</VirtualHost>

再次檢查並套用:

sudo apache2ctl configtest
sudo systemctl reload apache2

設定完成後:

  • %a 是 mod_remoteip 還原的 Visitor IP。
  • %{c}a 是實際連入 Apache 的 Cloudflare peer IP。
  • CGI、PHP 或其他使用 Apache request client address 的程式,通常會在 REMOTE_ADDR 取得還原後的 IP;實際結果仍應在 Application log 驗證。

只有 RemoteIPHeader、沒有 RemoteIPTrustedProxyList,等於沒有明確限制誰能提供 Client IP。Trusted Proxy List 必須保留。Apache HTTP Server:RemoteIPTrustedProxyList

systemd 每日更新 Trusted Proxy List

GCP Firewall 的 Cloudflare CIDR 仍由 Cloud Run Job 與 Cloud Scheduler 維護。本節的 systemd timer 只更新 VM 內的 Nginx 或 Apache2 trusted proxy list。

建立 service:

sudo nano /etc/systemd/system/cloudflare-trusted-proxy-update.service

Nginx 範例:

[Unit]
Description=Update Cloudflare trusted proxy list
Wants=network-online.target
After=network-online.target

[Service]
Type=oneshot
ExecStart=/usr/bin/flock --nonblock /run/cloudflare-trusted-proxy-update.lock /usr/local/sbin/update-cloudflare-nginx-real-ip
TimeoutStartSec=120

使用 Apache2 時,將 ExecStart 的最後一個路徑改成:

/usr/local/sbin/update-cloudflare-apache-real-ip

建立 timer:

sudo nano /etc/systemd/system/cloudflare-trusted-proxy-update.timer
[Unit]
Description=Daily Cloudflare trusted proxy list update

[Timer]
OnCalendar=daily
Persistent=true
RandomizedDelaySec=30m

[Install]
WantedBy=timers.target

檢查並啟用:

sudo systemd-analyze verify \
  /etc/systemd/system/cloudflare-trusted-proxy-update.service \
  /etc/systemd/system/cloudflare-trusted-proxy-update.timer

sudo systemctl daemon-reload
sudo systemctl start cloudflare-trusted-proxy-update.service
sudo systemctl enable --now cloudflare-trusted-proxy-update.timer

systemctl list-timers cloudflare-trusted-proxy-update.timer
sudo journalctl \
  --unit=cloudflare-trusted-proxy-update.service \
  --lines=50 \
  --no-pager

API request、JSON schema 或非空檢查失敗時,script 會在寫入設定前結束。清單沒有變動時不會 reload Web Server。

驗證 Visitor IP、Peer IP 與 Header Spoofing

正常 Cloudflare 路徑

從一般外部網路送出帶有唯一識別值的 request:

REAL_IP_TEST_ID="real-ip-test-$(date -u +%Y%m%dT%H%M%SZ)-${RANDOM}"

curl --noproxy '*' \
  --silent \
  --show-error \
  --output /dev/null \
  "https://www.example.com/healthz?real_ip_test=${REAL_IP_TEST_ID}"

printf 'Access Log 應出現:%s\n' "$REAL_IP_TEST_ID"

Nginx:

sudo grep --fixed-strings 'real-ip-test-REPLACE_WITH_ACTUAL_ID' \
  /var/log/nginx/www.example.com.access.log

Apache2:

sudo grep --fixed-strings 'real-ip-test-REPLACE_WITH_ACTUAL_ID' \
  /var/log/apache2/www.example.com.access.log

預期格式:

visitor=198.51.100.25 peer=173.245.48.10 cf_ip=198.51.100.25 cf_ray=...

檢查重點不是 IP 是否剛好和範例相同,而是:

  • visitor 與測試端的公開 IP 相符。
  • peer 位於 Cloudflare 官方 CIDR。
  • visitor 與 cf_ip 相符。
  • CF-Ray 有值,且 request 確實出現在 Origin Access Log。

/healthz 必須替換成明確不經 Cloudflare cache 的 endpoint;只看到 Cloudflare response,不能證明 request 已抵達 Origin。

VM 本機偽造 Header

在 Origin VM 從 loopback 送出偽造 Header:

curl --noproxy '*' \
  --insecure \
  --silent \
  --show-error \
  --dump-header - \
  --output /dev/null \
  --resolve 'www.example.com:443:127.0.0.1' \
  --header 'CF-Connecting-IP: 198.51.100.77' \
  'https://www.example.com/'

--insecure 僅用於 VM 本機、自己管理的 Origin 測試。正式監控不應略過 TLS certificate validation。

若 127.0.0.1 沒有被加入 trusted proxy list,預期結果如下:

  • Nginx $remote_addr 仍是 127.0.0.1
  • Apache2 %a 仍是 127.0.0.1
  • CF-Connecting-IP 雖然存在,但不能改寫 Client IP。

若 log 顯示 Visitor IP 被改成 198.51.100.77,代表 trusted proxy 範圍過大,或另一層 Application 又自行解析了 Header。

驗證矩陣

測試 Visitor IP Peer IP 預期結果
經 Cloudflare 的新 request 外部測試端 IP Cloudflare Edge IP Request 成功並同時記錄兩種 IP
外部 direct Origin 不適用 外部測試端 IP 被 GCP Firewall 阻擋,通常為 timeout
VM 本機偽造 CF-Connecting-IP 127.0.0.1 127.0.0.1 Header 不得改寫 Client IP
API 回傳失敗或空清單 沿用 Web Server 已載入的設定 不變 updater failed,不 reload

來源限制位於 Web Server 時

前面的 Nginx 與 Apache2 設定都假設 GCP Firewall 已經只允許 Cloudflare CIDR。Firewall 直接使用封包的 source IP,不受 Web Server 改寫 Client IP 的影響。

無法管理 GCP Firewall、來源限制改設在 Web Server 時,必須處理 real-IP module 的執行結果:

real-IP module 執行前:Client IP = Cloudflare peer
real-IP module 執行後:Client IP = Visitor IP

Nginx allow Cloudflare-CIDR 與 Apache2 Require ip Cloudflare-CIDR 都可能改為比對 Visitor IP,使正常的 Cloudflare request 被拒絕。這種架構的來源 gate 必須明確使用原始 connection peer。

Nginx 使用 $realip_remote_addr

Nginx 的 $remote_addr 會在 Real IP Module 執行後變成 Visitor IP;$realip_remote_addr 則保留實際連入 Nginx 的 connection peer。來源限制因此要用 $realip_remote_addr 判斷,不能沿用上一篇直接比對 $remote_addr 的 allowdeny

完整的 /etc/nginx/conf.d/cloudflare-real-ip.conf 如下:

include /etc/nginx/snippets/cloudflare-real-ip-trusted.conf;
real_ip_header CF-Connecting-IP;

geo $realip_remote_addr $cloudflare_peer {
    default 0;
    include /etc/nginx/snippets/cloudflare-peer-geo.conf;
}

log_format cloudflare_realip
    'visitor=$remote_addr peer=$realip_remote_addr '
    'cf_ip=$http_cf_connecting_ip cf_ray=$http_cf_ray '
    '"$request" $status $body_bytes_sent';

cloudflare-real-ip-trusted.conf 與 cloudflare-peer-geo.conf 都由本節稍後的完整 Updater 產生。前者決定哪些 peer 有權提供 CF-Connecting-IP;後者將同一份 Cloudflare CIDR 映射成 $cloudflare_peer=1

需要保護的 server block 使用以下設定:

server {
    listen 443 ssl;
    server_name www.example.com;

    access_log /var/log/nginx/www.example.com.access.log cloudflare_realip;

    if ($cloudflare_peer = 0) {
        return 403;
    }

    # 既有 TLS、root 或 proxy_pass 設定。
}

這裡的 if 只執行固定的 returngeo 可以指定 $realip_remote_addr 作為輸入,依 Cloudflare CIDR 判斷原始 peer。NGINX:Geo module

上一篇若已在這個 server block 加入 Cloudflare allow 清單與 deny all,必須移除,避免它們改用還原後的 Visitor IP 判斷。

Nginx Updater 完整版

用以下內容完整取代 /usr/local/sbin/update-cloudflare-nginx-real-ip

#!/usr/bin/env bash
set -Eeuo pipefail

if [[ "$EUID" -ne 0 ]]; then
  echo 'Run this updater as root.' >&2
  exit 1
fi

TRUSTED_FILE='/etc/nginx/snippets/cloudflare-real-ip-trusted.conf'
PEER_GEO_FILE='/etc/nginx/snippets/cloudflare-peer-geo.conf'
API_URL='https://api.cloudflare.com/client/v4/ips'
WORK_DIRECTORY="$(mktemp -d)"

cleanup() {
  rm -rf -- "$WORK_DIRECTORY"
}
trap cleanup EXIT

curl \
  --fail \
  --silent \
  --show-error \
  --connect-timeout 5 \
  --max-time 20 \
  --retry 3 \
  --output "$WORK_DIRECTORY/response.json" \
  "$API_URL"

jq --exit-status '
  .success == true
  and ((.result.ipv4_cidrs | type) == "array")
  and ((.result.ipv4_cidrs | length) > 0)
  and ((.result.ipv6_cidrs | type) == "array")
  and ((.result.ipv6_cidrs | length) > 0)
' "$WORK_DIRECTORY/response.json" >/dev/null

jq --raw-output '
  .result.ipv4_cidrs[],
  .result.ipv6_cidrs[]
' "$WORK_DIRECTORY/response.json" \
  | sort --unique > "$WORK_DIRECTORY/cloudflare-ips.lst"

[[ -s "$WORK_DIRECTORY/cloudflare-ips.lst" ]]

awk '{ print "set_real_ip_from " $0 ";" }' \
  "$WORK_DIRECTORY/cloudflare-ips.lst" \
  > "$WORK_DIRECTORY/cloudflare-real-ip-trusted.conf"

awk '{ print $0 " 1;" }' \
  "$WORK_DIRECTORY/cloudflare-ips.lst" \
  > "$WORK_DIRECTORY/cloudflare-peer-geo.conf"

install -d -m 0755 /etc/nginx/snippets

if [[ -f "$TRUSTED_FILE" && -f "$PEER_GEO_FILE" ]] \
  && cmp --silent "$WORK_DIRECTORY/cloudflare-real-ip-trusted.conf" "$TRUSTED_FILE" \
  && cmp --silent "$WORK_DIRECTORY/cloudflare-peer-geo.conf" "$PEER_GEO_FILE"; then
  echo 'Cloudflare Nginx IP files unchanged.'
  exit 0
fi

install -m 0644 \
  "$WORK_DIRECTORY/cloudflare-real-ip-trusted.conf" \
  "$TRUSTED_FILE"
install -m 0644 \
  "$WORK_DIRECTORY/cloudflare-peer-geo.conf" \
  "$PEER_GEO_FILE"

nginx -t
systemctl reload nginx

echo 'Cloudflare trusted proxy and peer allowlist updated for Nginx.'

設定執行權限並套用:

sudo chown root:root /usr/local/sbin/update-cloudflare-nginx-real-ip
sudo chmod 0755 /usr/local/sbin/update-cloudflare-nginx-real-ip
sudo /usr/local/sbin/update-cloudflare-nginx-real-ip
sudo nginx -t
sudo systemctl reload nginx

原本的 systemd service 不必修改,因為 Updater 路徑維持不變。每天更新時,Trusted Proxy List 與 peer allowlist 會從同一份 API response 一起產生。

Nginx 設定總結

設定 比對或輸出 用途
set_real_ip_from Cloudflare CIDR 限制誰能用 CF-Connecting-IP 改寫 Client IP
$remote_addr Visitor IP 提供 Access Log、Application 與分析使用
$realip_remote_addr Connection peer 保留實際連入 Nginx 的來源 IP
geo $realip_remote_addr Cloudflare CIDR 判斷 connection peer 是否屬於 Cloudflare
$cloudflare_peer = 0 非 Cloudflare peer 回覆 403,拒絕直連 Origin

這套設定同時處理兩件事:只有 Cloudflare 可以宣告 Visitor IP,且只有 Cloudflare connection peer 可以進入網站。兩項判斷共用同一批 CIDR,但用途與 Nginx 變數不同。

Apache2 使用 CONN_REMOTE_ADDR

Apache2 的 %a 與一般 Require ip 會使用 mod_remoteip 改寫後的 Visitor IP;CONN_REMOTE_ADDR 則保留底層 connection peer。來源限制要以 CONN_REMOTE_ADDR 搭配 -ipmatch 判斷。

/etc/apache2/conf-available/cloudflare-real-ip.conf 維持完整的 Real IP 與 log 設定:

RemoteIPHeader CF-Connecting-IP
RemoteIPTrustedProxyList /etc/apache2/cloudflare-ips.lst

LogFormat "visitor=%a peer=%{c}a cf_ip=%{CF-Connecting-IP}i cf_ray=%{CF-Ray}i \"%r\" %>s %b" cloudflare_realip

在目標 VirtualHost 載入 connection peer 的來源規則:

<VirtualHost *:443>
    ServerName www.example.com

    CustomLog ${APACHE_LOG_DIR}/www.example.com.access.log cloudflare_realip

    <Location "/">
        Include /etc/apache2/cloudflare-peer-require.conf
    </Location>

    # 既有 TLS、DocumentRoot 或 ProxyPass 設定。
</VirtualHost>

CONN_REMOTE_ADDR 是 connection peer;-ipmatch 依 IP 或 CIDR 比對。Apache HTTP Server:Expression parser

上一篇若已加入直接比對 Client IP 的 Require ip 規則,必須移除。相同 <Location><Directory> 或 Proxy 區段若另有 authorization 規則,也要檢查合併後的結果。

Apache2 Updater 完整版

用以下內容完整取代 /usr/local/sbin/update-cloudflare-apache-real-ip

#!/usr/bin/env bash
set -Eeuo pipefail

if [[ "$EUID" -ne 0 ]]; then
  echo 'Run this updater as root.' >&2
  exit 1
fi

TRUSTED_FILE='/etc/apache2/cloudflare-ips.lst'
PEER_REQUIRE_FILE='/etc/apache2/cloudflare-peer-require.conf'
API_URL='https://api.cloudflare.com/client/v4/ips'
WORK_DIRECTORY="$(mktemp -d)"

cleanup() {
  rm -rf -- "$WORK_DIRECTORY"
}
trap cleanup EXIT

curl \
  --fail \
  --silent \
  --show-error \
  --connect-timeout 5 \
  --max-time 20 \
  --retry 3 \
  --output "$WORK_DIRECTORY/response.json" \
  "$API_URL"

jq --exit-status '
  .success == true
  and ((.result.ipv4_cidrs | type) == "array")
  and ((.result.ipv4_cidrs | length) > 0)
  and ((.result.ipv6_cidrs | type) == "array")
  and ((.result.ipv6_cidrs | length) > 0)
' "$WORK_DIRECTORY/response.json" >/dev/null

jq --raw-output '
  .result.ipv4_cidrs[],
  .result.ipv6_cidrs[]
' "$WORK_DIRECTORY/response.json" \
  | sort --unique > "$WORK_DIRECTORY/cloudflare-ips.lst"

[[ -s "$WORK_DIRECTORY/cloudflare-ips.lst" ]]

{
  echo '<RequireAny>'
  while IFS= read -r cidr; do
    printf "    Require expr \"%%{CONN_REMOTE_ADDR} -ipmatch '%s'\"\n" "$cidr"
  done < "$WORK_DIRECTORY/cloudflare-ips.lst"
  echo '</RequireAny>'
} > "$WORK_DIRECTORY/cloudflare-peer-require.conf"

if [[ -f "$TRUSTED_FILE" && -f "$PEER_REQUIRE_FILE" ]] \
  && cmp --silent "$WORK_DIRECTORY/cloudflare-ips.lst" "$TRUSTED_FILE" \
  && cmp --silent "$WORK_DIRECTORY/cloudflare-peer-require.conf" "$PEER_REQUIRE_FILE"; then
  echo 'Cloudflare Apache2 IP files unchanged.'
  exit 0
fi

install -m 0644 \
  "$WORK_DIRECTORY/cloudflare-ips.lst" \
  "$TRUSTED_FILE"
install -m 0644 \
  "$WORK_DIRECTORY/cloudflare-peer-require.conf" \
  "$PEER_REQUIRE_FILE"

apache2ctl configtest
systemctl reload apache2

echo 'Cloudflare trusted proxy and peer allowlist updated for Apache2.'

設定執行權限並套用:

sudo chown root:root /usr/local/sbin/update-cloudflare-apache-real-ip
sudo chmod 0755 /usr/local/sbin/update-cloudflare-apache-real-ip
sudo /usr/local/sbin/update-cloudflare-apache-real-ip
sudo apache2ctl configtest
sudo systemctl reload apache2

原本的 systemd service 同樣不必修改。每天更新時,cloudflare-ips.lst 與 cloudflare-peer-require.conf 會由同一份 API response 產生。

Apache2 設定總結

設定 比對或輸出 用途
RemoteIPTrustedProxyList Cloudflare CIDR 限制誰能用 CF-Connecting-IP 改寫 Client IP
%a Visitor IP 提供 Access Log、Application 與分析使用
%{c}a Connection peer 在 Access Log 保留實際連入 Apache2 的來源 IP
CONN_REMOTE_ADDR -ipmatch Cloudflare CIDR 依 connection peer 執行來源限制
<RequireAny> 任一 Cloudflare CIDR 相符 允許 request;其餘來源回覆 403

Apache2 的 Trusted Proxy List 與來源限制共用同一批 CIDR,但判斷對象不同:RemoteIPTrustedProxyList 控制 Header trust,CONN_REMOTE_ADDR 控制實際 connection peer。

完成後重新執行兩項測試:正常 Cloudflare request 必須成功;VM 本機偽造 CF-Connecting-IP 的 request 必須回覆 403

設定邊界與常見錯誤

Application 不應再次解析未驗證 Header

Nginx $remote_addr、Apache2 %a 或 Application 的 REMOTE_ADDR,應是 trusted proxy 驗證後的結果。若 Framework 又直接取用 X-Forwarded-For 第一個值,可能重新引入 Header spoofing。

Visitor IP 適合用於 log、異常偵測、Rate Limit 輸入與 GeoIP,但不應作為永久帳號識別或唯一授權條件。NAT、VPN、行動網路與共用 Proxy 都會讓多位使用者共用 IP,IPv6 privacy address 也可能改變。

Cloudflare Workers 與多層 Proxy

本文假設 Cloudflare 後方直接連到 Nginx 或 Apache2。若中間還有 Google Cloud Load Balancer、HAProxy、Ingress Controller、Service Mesh 或另一層 CDN,Web Server 的 TCP peer 將不再是 Cloudflare Edge;此時 trusted proxy chain、Header 覆寫位置與 X-Forwarded-For 解析方向都必須重新設計。

Cloudflare Workers subrequest 也有額外的 CF-Connecting-IP 行為。使用 Worker 改寫 request 或跨 zone 呼叫時,應依 Cloudflare Header 文件重新確認,不可直接套用本文的單層模型。

IPv4 與 IPv6 必須同時維護

Updater 會同時寫入 Cloudflare IPv4 與 IPv6 CIDR。即使 Origin 目前只有 IPv4,保留 IPv6 trusted list 不會自行建立 IPv6 listener;未來啟用 IPv6 時則不會漏掉 Proxy trust。

set_real_ip_from 0.0.0.0/0::/0,或把整個 VPC、所有 RFC 1918 網段加入 trusted list,都會擴大 Header 的信任來源。只有實際會轉送 Cloudflare request 的 peer 才應列入。

Full (strict) 不處理 Client IP

Full (strict) 驗證的是 Cloudflare 連到 Origin 時,Origin 提供的 certificate 是否有效;它不負責還原 Visitor IP,也不限制誰能提供 CF-Connecting-IP。Client IP、來源限制與 Origin TLS 是三個不同控制點。

完成設定後,Origin 應能同時回答:

誰實際連進來?       → peer IP
Cloudflare 代表誰轉送? → visitor IP
誰有權提供 visitor IP? → trusted proxy list